package xyz.fivefaith.tool.common.util;

import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.jetbrains.annotations.Nullable;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Stream;

/**
 * 通用工具类
 *
 * @author zhaoanchi
 * @version 1.0
 * @since 2023-11-10 10:01
 */
@UtilityClass
public class Kit {

    /* ****************************** Convert **************************** */

    /**
     * 强转string,并去掉多余空格
     *
     * @param str 字符串
     * @return String
     */
    public static String toStr(Object str) {
        return toStr(str, "");
    }

    /**
     * 强转string,并去掉多余空格
     *
     * @param str 字符串
     * @param defaultValue 默认值
     * @return String
     */
    public static String toStr(Object str, String defaultValue) {
        if (null == str) {
            return defaultValue;
        }
        return String.valueOf(str);
    }

    /**
     * Convert a <code>String</code> to an <code>int</code>, returning <code>zero</code> if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, <code>zero</code> is returned.
     *
     * <pre>
     *   $.toInt(null) = 0
     *   $.toInt("")   = 0
     *   $.toInt("1")  = 1
     * </pre>
     *
     * @param value the string to convert, may be null
     * @return the int represented by the string, or <code>zero</code> if conversion fails
     */
    public static int toInt(final Object value) {
        return toInt(String.valueOf(value));
    }

    /**
     * Convert a <code>String</code> to an <code>int</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   $.toInt(null, 1) = 1
     *   $.toInt("", 1)   = 1
     *   $.toInt("1", 0)  = 1
     * </pre>
     *
     * @param value the string to convert, may be null
     * @param defaultValue the default value
     * @return the int represented by the string, or the default if conversion fails
     */
    public static int toInt(final Object value, final int defaultValue) {
        return toInt(String.valueOf(value), defaultValue);
    }

    /**
     * Convert a <code>String</code> to an <code>Double</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   $.toDouble(null, 1) = 1.0
     *   $.toDouble("", 1)   = 1.0
     *   $.toDouble("1", 0)  = 1.0
     * </pre>
     *
     * @param value the string to convert, may be null
     * @return the int represented by the string, or the default if conversion fails
     */
    public static Double toDouble(Object value) {
        return toDouble(String.valueOf(value), -1.00);
    }

    /**
     * Convert a <code>String</code> to an <code>Double</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   $.toDouble(null, 1) = 1.0
     *   $.toDouble("", 1)   = 1.0
     *   $.toDouble("1", 0)  = 1.0
     * </pre>
     *
     * @param value the string to convert, may be null
     * @param defaultValue the default value
     * @return the int represented by the string, or the default if conversion fails
     */
    public static Double toDouble(Object value, Double defaultValue) {
        return toDouble(String.valueOf(value), defaultValue);
    }

    /**
     * Convert a <code>String</code> to a <code>long</code>, returning <code>zero</code> if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, <code>zero</code> is returned.
     *
     * <pre>
     *   $.toLong(null) = 0L
     *   $.toLong("")   = 0L
     *   $.toLong("1")  = 1L
     * </pre>
     *
     * @param value the string to convert, may be null
     * @return the long represented by the string, or <code>0</code> if conversion fails
     */
    public static long toLong(final Object value) {
        return toLong(String.valueOf(value));
    }

    /**
     * Convert a <code>String</code> to a <code>long</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   $.toLong(null, 1L) = 1L
     *   $.toLong("", 1L)   = 1L
     *   $.toLong("1", 0L)  = 1L
     * </pre>
     *
     * @param value the string to convert, may be null
     * @param defaultValue the default value
     * @return the long represented by the string, or the default if conversion fails
     */
    public static long toLong(final Object value, final long defaultValue) {
        return toLong(String.valueOf(value), defaultValue);
    }

    /* **************************************** Base **************************************** */

    /**
     * Convert a <code>String</code> to an <code>int</code>, returning <code>zero</code> if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, <code>zero</code> is returned.
     *
     * <pre>
     *   NumberUtil.toInt(null) = 0
     *   NumberUtil.toInt("")   = 0
     *   NumberUtil.toInt("1")  = 1
     * </pre>
     *
     * @param str the string to convert, may be null
     * @return the int represented by the string, or <code>zero</code> if conversion fails
     */
    public static int toInt(final String str) {
        return toInt(str, -1);
    }

    /**
     * Convert a <code>String</code> to an <code>int</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   NumberUtil.toInt(null, 1) = 1
     *   NumberUtil.toInt("", 1)   = 1
     *   NumberUtil.toInt("1", 0)  = 1
     * </pre>
     *
     * @param str the string to convert, may be null
     * @param defaultValue the default value
     * @return the int represented by the string, or the default if conversion fails
     */
    public static int toInt(@Nullable final String str, final int defaultValue) {
        if (str == null) {
            return defaultValue;
        }
        try {
            return Integer.parseInt(str);
        } catch (final NumberFormatException nfe) {
            return defaultValue;
        }
    }

    /**
     * Convert a <code>String</code> to a <code>long</code>, returning <code>zero</code> if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, <code>zero</code> is returned.
     *
     * <pre>
     *   NumberUtil.toLong(null) = 0L
     *   NumberUtil.toLong("")   = 0L
     *   NumberUtil.toLong("1")  = 1L
     * </pre>
     *
     * @param str the string to convert, may be null
     * @return the long represented by the string, or <code>0</code> if conversion fails
     */
    public static long toLong(final String str) {
        return toLong(str, 0L);
    }

    /**
     * Convert a <code>String</code> to a <code>long</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   NumberUtil.toLong(null, 1L) = 1L
     *   NumberUtil.toLong("", 1L)   = 1L
     *   NumberUtil.toLong("1", 0L)  = 1L
     * </pre>
     *
     * @param str the string to convert, may be null
     * @param defaultValue the default value
     * @return the long represented by the string, or the default if conversion fails
     */
    public static long toLong(@Nullable final String str, final long defaultValue) {
        if (str == null) {
            return defaultValue;
        }
        try {
            return Long.parseLong(str);
        } catch (final NumberFormatException nfe) {
            return defaultValue;
        }
    }

    /**
     * Convert a <code>String</code> to a <code>Double</code>
     *
     * @param value value
     * @return double value
     */
    public static Double toDouble(String value) {
        return toDouble(value, null);
    }

    /**
     * Convert a <code>String</code> to a <code>Double</code>
     *
     * @param value value
     * @param defaultValue 默认值
     * @return double value
     */
    public static Double toDouble(@Nullable String value, Double defaultValue) {
        if (value != null) {
            return Double.valueOf(value.trim());
        }
        return defaultValue;
    }

    /**
     * Convert a <code>String</code> to a <code>Double</code>
     *
     * @param value value
     * @return double value
     */
    public static Float toFloat(String value) {
        return toFloat(value, null);
    }

    /**
     * Convert a <code>String</code> to a <code>Double</code>
     *
     * @param value value
     * @param defaultValue 默认值
     * @return double value
     */
    public static Float toFloat(@Nullable String value, Float defaultValue) {
        if (value != null) {
            return Float.valueOf(value.trim());
        }
        return defaultValue;
    }

    /**
     * Convert a <code>String</code> to an <code>Float</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   $.toFloat(null, 1) = 1.00f
     *   $.toFloat("", 1)   = 1.00f
     *   $.toFloat("1", 0)  = 1.00f
     * </pre>
     *
     * @param value the string to convert, may be null
     * @return the int represented by the string, or the default if conversion fails
     */
    public static Float toFloat(Object value) {
        return toFloat(String.valueOf(value), -1.0f);
    }

    /**
     * Convert a <code>String</code> to an <code>Float</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   $.toFloat(null, 1) = 1.00f
     *   $.toFloat("", 1)   = 1.00f
     *   $.toFloat("1", 0)  = 1.00f
     * </pre>
     *
     * @param value the string to convert, may be null
     * @param defaultValue the default value
     * @return the int represented by the string, or the default if conversion fails
     */
    public static Float toFloat(Object value, Float defaultValue) {
        return toFloat(String.valueOf(value), defaultValue);
    }

    /**
     * Convert a <code>String</code> to an <code>Boolean</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   $.toBoolean("true", true)  = true
     *   $.toBoolean("false")   	= false
     *   $.toBoolean("", false)  	= false
     * </pre>
     *
     * @param value the string to convert, may be null
     * @return the int represented by the string, or the default if conversion fails
     */
    public static Boolean toBoolean(Object value) {
        return toBoolean(value, null);
    }

    /**
     * Convert a <code>String</code> to an <code>Boolean</code>, returning a default value if the
     * conversion fails.
     *
     * <p>If the string is <code>null</code>, the default value is returned.
     *
     * <pre>
     *   $.toBoolean("true", true)  = true
     *   $.toBoolean("false")   	= false
     *   $.toBoolean("", false)  	= false
     * </pre>
     *
     * @param value the string to convert, may be null
     * @param defaultValue the default value
     * @return the int represented by the string, or the default if conversion fails
     */
    public static Boolean toBoolean(Object value, Boolean defaultValue) {
        if (value != null) {
            String val = String.valueOf(value);
            val = val.toLowerCase().trim();
            return Boolean.parseBoolean(val);
        }
        return defaultValue;
    }

    /** All possible chars for representing a number as a String */
    private static final char[] DIGITS = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
        'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
        'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
    };

    /**
     * 将 long 转短字符串 为 62 进制
     *
     * @param i 数字
     * @return 短字符串
     */
    public static String to62String(long i) {
        int radix = DIGITS.length;
        char[] buf = new char[65];
        int charPos = 64;
        i = -i;
        while (i <= -radix) {
            buf[charPos--] = DIGITS[(int) (-(i % radix))];
            i = i / radix;
        }
        buf[charPos] = DIGITS[(int) (-i)];

        return new String(buf, charPos, (65 - charPos));
    }

    /* ****************************** Convert **************************** */

    /* ****************************** StringUtil **************************** */
    /**
     * Determine whether the given object is empty: i.e. {@code null} or of zero length.
     *
     * @param obj the object to check
     * @return 数组是否为空
     */
    public static boolean isEmpty(@Nullable Object obj) {
        return ObjectUtils.isEmpty(obj);
    }

    /**
     * Determine whether the given object is not empty: i.e. {@code null} or of zero length.
     *
     * @param obj the object to check
     * @return 是否不为空
     */
    public static boolean isNotEmpty(@Nullable Object obj) {
        return !ObjectUtils.isEmpty(obj);
    }

    /**
     * 判断数组不为空
     *
     * @param array 数组
     * @return 数组是否不为空
     */
    public static boolean isNotEmpty(@Nullable Object[] array) {
        return ObjectUtils.isNotEmpty(array);
    }

    /**
     * Determine whether the given array is empty: i.e. {@code null} or of zero length.
     *
     * @param array the array to check
     * @return 数组是否为空
     */
    public static boolean isEmpty(@Nullable Object[] array) {
        return ObjectUtils.isEmpty(array);
    }

    /**
     * 对象组中是否存在 Empty Object
     *
     * @param os 对象组
     * @return boolean
     */
    public static boolean hasEmpty(Object... os) {
        for (Object o : os) {
            if (isEmpty(o)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 对象组中是否全是 Empty Object
     *
     * @param os 对象组
     * @return boolean
     */
    public static boolean allEmpty(Object... os) {
        for (Object o : os) {
            if (!isEmpty(o)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Check whether the given {@code CharSequence} contains actual <em>text</em>.
     *
     * <p>More specifically, this method returns {@code true} if the {@code CharSequence} is not
     * {@code null}, its length is greater than 0, and it contains at least one non-whitespace
     * character.
     *
     * <pre class="code">
     * $.isBlank(null)		= true
     * $.isBlank("")		= true
     * $.isBlank(" ")		= true
     * $.isBlank("12345")	= false
     * $.isBlank(" 12345 ")	= false
     * </pre>
     *
     * @param cs the {@code CharSequence} to check (may be {@code null})
     * @return {@code true} if the {@code CharSequence} is not {@code null}, its length is greater
     *     than 0, and it does not contain whitespace only
     * @see Character#isWhitespace
     */
    public static boolean isBlank(@Nullable final CharSequence cs) {
        return StringUtils.isBlank(cs);
    }

    /**
     * Checks if a CharSequence is not empty (""), not null and not whitespace only.
     *
     * <pre>
     * $.isNotBlank(null)	= false
     * $.isNotBlank("")		= false
     * $.isNotBlank(" ")	= false
     * $.isNotBlank("bob")	= true
     * $.isNotBlank("  bob  ") = true
     * </pre>
     *
     * @param cs the CharSequence to check, may be null
     * @return {@code true} if the CharSequence is not empty and not null and not whitespace
     * @see Character#isWhitespace
     */
    public static boolean isNotBlank(@Nullable final CharSequence cs) {
        return StringUtils.isNotBlank(cs);
    }

    /**
     * 有 任意 一个 Blank
     *
     * @param css CharSequence
     * @return boolean
     */
    public static boolean isAnyBlank(final CharSequence... css) {
        if (ObjectUtils.isEmpty(css)) {
            return true;
        }
        return Stream.of(css).anyMatch(StringUtils::isBlank);
    }

    /**
     * 是否全非 Blank
     *
     * @param css CharSequence
     * @return boolean
     */
    public static boolean isNoneBlank(final CharSequence... css) {
        if (ObjectUtils.isEmpty(css)) {
            return false;
        }
        return Stream.of(css).allMatch(StringUtils::isNotBlank);
    }

    /**
     * 比较两个对象是否相等。<br>
     * 相同的条件有两个，满足其一即可：<br>
     *
     * @param obj1 对象1
     * @param obj2 对象2
     * @return 是否相等
     */
    public static boolean equals(Object obj1, Object obj2) {
        return Objects.equals(obj1, obj2);
    }

    /**
     * Determine if the given objects are equal, returning {@code true} if both are {@code null} or
     * {@code false} if only one is {@code null}.
     *
     * <p>Compares arrays with {@code Arrays.equals}, performing an equality check based on the
     * array elements rather than the array reference.
     *
     * @param o1 first Object to compare
     * @param o2 second Object to compare
     * @return whether the given objects are equal
     * @see Object#equals(Object)
     * @see Arrays#equals
     */
    public static boolean equalsSafe(@Nullable Object o1, @Nullable Object o2) {
        return nullSafeEquals(o1, o2);
    }

    /**
     * 创建StringBuilder对象
     *
     * @param sb 初始StringBuilder
     * @param strs 初始字符串列表
     * @return StringBuilder对象
     */
    public static StringBuilder appendBuilder(StringBuilder sb, CharSequence... strs) {
        for (CharSequence str : strs) {
            sb.append(str);
        }
        return sb;
    }

    /**
     * 转换为String数组<br>
     *
     * @param str 被转换的值
     * @return 结果
     */
    public static String[] toStrArray(String str) {
        return toStrArray(",", str);
    }

    /**
     * 转换为String数组<br>
     *
     * @param split 分隔符
     * @param str 被转换的值
     * @return 结果
     */
    public static String[] toStrArray(String split, String str) {
        if (StringUtils.isBlank(str)) {
            return new String[] {};
        }
        return str.split(split);
    }

    /**
     * 获取标识符，用于参数清理
     *
     * @param param 参数
     * @return 清理后的标识符
     */
    @Nullable
    public static String cleanIdentifier(@Nullable String param) {
        if (param == null) {
            return null;
        }
        StringBuilder paramBuilder = new StringBuilder();
        for (int i = 0; i < param.length(); i++) {
            char c = param.charAt(i);
            if (Character.isJavaIdentifierPart(c)) {
                paramBuilder.append(c);
            }
        }
        return paramBuilder.toString();
    }

    /**
     * 隐藏[startInclude, endExclude) 为 *
     *
     * @param str 字符串
     * @param startInclude 起始索引, 0起点
     * @param endExclude 结束索引
     * @return 新字符串
     */
    public static String hide(String str, int startInclude, int endExclude) {
        return StringUtils.overlay(
                str, StringUtils.repeat('*', endExclude - startInclude), startInclude, endExclude);
    }

    /**
     * 隐藏[startInclude, endExclude) 为 replacedChar
     *
     * @param str 字符串
     * @param startInclude 起始索引, 0起点
     * @param endExclude 结束索引
     * @return 新字符串
     */
    public static String hide(String str, int startInclude, int endExclude, char replacedChar) {
        return StringUtils.overlay(
                str,
                StringUtils.repeat(replacedChar, endExclude - startInclude),
                startInclude,
                endExclude);
    }

    /**
     * 去掉后缀
     *
     * @param str str
     * @param suffix suffix
     * @return String
     */
    public static String removeSuffix(String str, String suffix) {
        if (str == null || suffix == null || !str.endsWith(suffix)) {
            return str;
        }
        return str.substring(0, str.length() - suffix.length());
    }

    /**
     * 驼峰转下划线
     *
     * @param camel camel
     * @return String
     */
    public static String toUnderlineCase(String camel) {
        if (camel == null) {
            return null;
        }
        return camel.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
    }

    /* ****************************** StringUtil **************************** */

    /* ****************************** ObjectUtil **************************** */

    public static <T> T notNull(T obj, T defaultValue) {
        if (ObjectUtils.isNotEmpty(obj)) {
            return obj;
        }
        return defaultValue;
    }

    /**
     * Determine if the given objects are equal, returning {@code true} if both are {@code null} or
     * {@code false} if only one is {@code null}.
     *
     * <p>Compares arrays with {@code Arrays.equals}, performing an equality check based on the
     * array elements rather than the array reference.
     *
     * @param o1 first Object to compare
     * @param o2 second Object to compare
     * @return whether the given objects are equal
     * @see Object#equals(Object)
     * @see java.util.Arrays#equals
     */
    public static boolean nullSafeEquals(@Nullable Object o1, @Nullable Object o2) {
        if (o1 == o2) {
            return true;
        }
        if (o1 == null || o2 == null) {
            return false;
        }
        if (o1.equals(o2)) {
            return true;
        }
        if (o1.getClass().isArray() && o2.getClass().isArray()) {
            return arrayEquals(o1, o2);
        }
        return false;
    }

    /**
     * Compare the given arrays with {@code Arrays.equals}, performing an equality check based on
     * the array elements rather than the array reference.
     *
     * @param o1 first array to compare
     * @param o2 second array to compare
     * @return whether the given objects are equal
     * @see #nullSafeEquals(Object, Object)
     * @see java.util.Arrays#equals
     */
    private static boolean arrayEquals(Object o1, Object o2) {
        if (o1 instanceof Object[] && o2 instanceof Object[]) {
            return Arrays.equals((Object[]) o1, (Object[]) o2);
        }
        if (o1 instanceof boolean[] && o2 instanceof boolean[]) {
            return Arrays.equals((boolean[]) o1, (boolean[]) o2);
        }
        if (o1 instanceof byte[] && o2 instanceof byte[]) {
            return Arrays.equals((byte[]) o1, (byte[]) o2);
        }
        if (o1 instanceof char[] && o2 instanceof char[]) {
            return Arrays.equals((char[]) o1, (char[]) o2);
        }
        if (o1 instanceof double[] && o2 instanceof double[]) {
            return Arrays.equals((double[]) o1, (double[]) o2);
        }
        if (o1 instanceof float[] && o2 instanceof float[]) {
            return Arrays.equals((float[]) o1, (float[]) o2);
        }
        if (o1 instanceof int[] && o2 instanceof int[]) {
            return Arrays.equals((int[]) o1, (int[]) o2);
        }
        if (o1 instanceof long[] && o2 instanceof long[]) {
            return Arrays.equals((long[]) o1, (long[]) o2);
        }
        if (o1 instanceof short[] && o2 instanceof short[]) {
            return Arrays.equals((short[]) o1, (short[]) o2);
        }
        return false;
    }

    /* ****************************** ObjectUtil **************************** */

    /* ****************************** IdUtil **************************** */

    public static String getSimpleUuid() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    /* ****************************** IdUtil **************************** */

    /* ****************************** ReflectUtils **************************** */

    @SuppressWarnings("unchecked")
    public static <E> E invokeGetter(Object obj, String fieldName) {
        try {
            String getterName =
                    (isBoolean(fieldName) ? "is" : "get")
                            + Character.toUpperCase(fieldName.charAt(0))
                            + fieldName.substring(1);
            return (E) MethodUtils.invokeMethod(obj, getterName);
        } catch (Exception e) {
            throw new IllegalStateException("无法读取字段 " + fieldName, e);
        }
    }

    private static boolean isBoolean(String field) {
        // 简单规则：字段名以 'is' 开头且长度 > 2 的认为是 boolean
        return field.startsWith("is") && field.length() > 2;
    }

    public static <T extends Annotation> T getAnnotation(Field field, Class<T> annotationClass) {
        if (field == null) {
            return null;
        }
        return field.isAnnotationPresent(annotationClass)
                ? field.getAnnotation(annotationClass)
                : null;
    }

    /* ****************************** ReflectUtils **************************** */

    /* ****************************** EnumUtils **************************** */

    /**
     * 获取枚举指定字段的值列表
     *
     * @param clazz 枚举类
     * @param fieldName 字段名（如 "code"、"text"）
     * @return 字段值列表
     */
    @SneakyThrows({IllegalAccessException.class})
    public static List<Object> getEnum(Class<? extends Enum<?>> clazz, String fieldName) {
        if (null == clazz || StringUtils.isBlank(fieldName)) {
            return null;
        }
        final Enum<?>[] enums = clazz.getEnumConstants();
        if (null == enums) {
            return null;
        }
        final List<Object> list = new ArrayList<>(enums.length);
        for (Enum<?> e : enums) {
            list.add(FieldUtils.readField(e, fieldName, true));
        }
        return list;
    }

    /* ****************************** EnumUtils **************************** */

}
